package com.googlecode.jaks.swt;

import static com.googlecode.jaks.common.io.DigestOutputStream.MD5;
import static com.googlecode.jaks.common.io.StreamUtil.UTF8;
import static com.googlecode.jaks.common.io.StreamUtil.transfer;
import static com.googlecode.jaks.common.util.StringUtil.isEmpty;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectHelper;

import com.googlecode.jaks.common.io.DigestOutputStream;
import com.googlecode.jaks.common.io.MultiOutputStream;
import com.googlecode.jaks.common.io.NonClosingInputStream;
import com.googlecode.jaks.common.io.StreamUtil;

/**
 * 
 * @author Jason Smith
 * @requiresDependencyResolution compile
 * @goal install-artifact-from-http
 * @phase package
 */
public class InstallArtifactFromHttpMojo extends AbstractMojo 
{
	/**
	 * The current project.
	 * @parameter expression="${project}"
	 * @readonly
	 */
	protected final org.apache.maven.project.MavenProject project = null;
	
	/**
	 * @component
	 * @readonly
	 */
	protected MavenProjectHelper projectHelper = null;
	
	/**
	 * The type of artifact.
	 * <br/><strong>Default</strong>: <tt>&lt;null&gt;</tt>
	 * @parameter
	 */
	protected String classifier = null;
	
	/**
	 * The type of artifact.
	 * <br/><strong>Default</strong>: <tt>jar</tt>
	 * @parameter
	 */
	protected String type = "jar";
	
	/**
	 * A list of acceptable archive mirrors. The mojo will pick one at random.
	 * @parameter 
	 */
	protected List<String> archives = null;
	
	
	/**
	 * The SHA1 keys, by classifier. Usually in the form 
	 * <tt>8b54457871fe031832a5fe2491f046a519b2bb1b  swt-4.2.1-win32-win32-x86_64.zip</tt>,
	 * where the trailing part is ignored.
	 * @parameter
	 */
	protected String sha1;
	
	/**
	 * The MD5 keys, by classifier. Usually in the form 
	 * <tt>871d78bae52c2995739a14049c65a0e1  swt-4.2.1-gtk-linux-x86_64.zip</tt>,
	 * @parameter
	 */
	protected String md5;
	
    /**
     * @component
     * @noinspection UnusedDeclaration
     */
    protected InstallerSharedState state;

	@Override
	public void execute() throws MojoExecutionException, MojoFailureException 
	{
		try
		{
			final File targetDir = new File(project.getBuild().getDirectory());
			targetDir.mkdir();
			
			final File archive = getArchiveFromHttp();
			processArchive(archive);
		}
		catch(final Exception e)
		{
			throw new MojoExecutionException("I've lost me mojo!", e);
		}
	}
	
	protected void processArchive(final File archive) throws FileNotFoundException, IOException
	{
		attachArtifact(project, "jar", classifier, archive);
	}
	
	protected File unzipArchive(final File archive) throws FileNotFoundException, IOException
	{
		final File folder = new File(getTargetDir(), project.getArtifactId() + "-" + project.getVersion() + "-" + classifier);
		folder.mkdir();
		
		unzip(new ZipInputStream(new BufferedInputStream(new FileInputStream(archive))), folder);
		
		return folder;
	}
	
	/**
	 * Get the project target directory.
	 * @return The project target directory.
	 */
	protected File getTargetDir()
	{
		final File targetDir = new File(project.getBuild().getDirectory());
		targetDir.mkdir();
		return targetDir;
	}
	
	/**
	 * Grab the archive from one of the specified URLs at random and check it against
	 * the SHA-1 and/or MD5 hash.
	 * @return The downloaded archive.
	 * @throws Exception See {@link Exception}.
	 */
	protected File getArchiveFromHttp() throws Exception
	{
		if(archives.isEmpty())
		{
			throw new IllegalArgumentException("No archive mirrors were specified.");
		}
		
		for(final String archiveUrl : archives)
		{
			if(state.artifacts.containsKey(archiveUrl))
			{
				return state.artifacts.get(archiveUrl);
			}
			
			final File archive = getArchiveFile(archiveUrl);
			if(archive.isFile())
			{
				state.artifacts.put(archiveUrl, archive);
				return archive;
			}
		}
		
		final List<String> shuffled = new ArrayList<>(archives);
		Collections.shuffle(shuffled);
		final String archiveUrl = shuffled.get(0);
		
		getLog().info("Downloading " + archiveUrl);
		
		final DigestOutputStream sha1Digest = new DigestOutputStream("SHA-1");
		final DigestOutputStream md5Digest = new DigestOutputStream("MD5");
		
		final File archive = getArchiveFile(archiveUrl);
		
		transfer(
				new URL(archiveUrl).openStream(), 
				new MultiOutputStream(new FileOutputStream(archive.getCanonicalFile()), sha1Digest, md5Digest));
		
		try
		{
			if(isEmpty(sha1) && isEmpty(md5))
			{
				throw new SecurityException("You must specify either SHA-1 or MD5 digest.");
			}
			
			if(!isEmpty(sha1))
			{
				if(!sha1.startsWith(sha1Digest.getDigestString()))
				{
					throw new SecurityException("SHA-1 digest mismatch. " +
							"Expected: " + sha1 + " " +
							"Actual: " + sha1Digest.getDigestString());
				}
				else
				{
					getLog().info("SHA-1 digest verified.");
				}
			}
			
			if(!isEmpty(md5))
			{
				if(!md5.startsWith(md5Digest.getDigestString()))
				{
					throw new SecurityException("MD5 digest mismatch. " +
							"Expected: " + md5 + " " +
							"Actual: " + md5Digest.getDigestString());
				}
				else
				{
					getLog().info("MD5 digest verified.");
				}
			}
		}
		catch(final SecurityException e)
		{
			/*
			 * Don't let a file with bad checksum hang around. It will be reused, without
			 * the check, next build.
			 */
			archive.delete();
			throw e;
		}
		
		state.artifacts.put(archiveUrl, archive);
		return archive;
	}
	
	/**
	 * Get the location for the archive in the current target folder.
	 * @param archiveUrl The archive URL.
	 * @return
	 * @throws NoSuchAlgorithmException
	 * @throws UnsupportedEncodingException
	 * @throws IOException
	 */
	protected File getArchiveFile(final String archiveUrl) throws NoSuchAlgorithmException, UnsupportedEncodingException, IOException
	{
		final DigestOutputStream digest = new DigestOutputStream(MD5);
		transfer(new ByteArrayInputStream(archiveUrl.getBytes(UTF8)), digest);
		final File archiveFile = new File(getTargetDir(), getClass().getSimpleName() + "-archive-" + digest.getDigestString() + ".arc");
		return archiveFile;
	}
	
	protected void unzip(final ZipInputStream source, final File target) throws IOException
	{
		try
		{
			if(!target.isDirectory())
			{
				throw new FileNotFoundException(target.getPath());
			}
			for(;;)
			{
				final ZipEntry entry = source.getNextEntry();
				if(entry == null)
				{
					break;
				}
				
				try
				{
					if(entry.isDirectory())
					{
						final File folder = new File(target, entry.getName());
						folder.mkdirs();
					}
					else
					{
						final File file = new File(target, entry.getName());
						file.getParentFile().mkdirs();
						StreamUtil.transfer(new NonClosingInputStream(source), new BufferedOutputStream(new FileOutputStream(file)));
					}
				}
				finally
				{
					source.closeEntry();
				}
			}
		}
		finally
		{
			source.close();
		}
	}
	
	protected void attachArtifact(final MavenProject project, final String type, final String classifier, final File artifactFile)
	{
		projectHelper.attachArtifact(project, type, classifier, artifactFile);
	}
}
